/**
 * Copyright (c), 上海沃恩信息科技有限公司.
 * All rights reserved.
 * Licensed under BSD (https://www.pittypat.work/bsd.html).
 */

package pittypat.web;

import pittypat.IJson;
import java.io.IOException;

import javax.websocket.CloseReason.CloseCodes;
import javax.websocket.Session;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * 用于处理长时间耗时的功能的 WebSocket。
 */
public final class FunctionSocket extends WebSocket {
	private static final Logger LOGGER = LogManager.getLogger(FunctionSocket.class);

	/**
	 * 请求查询字符串中用于标记客户端需要传送数据的指示参数的名称。
	 * <p>
	 * 通过 WebSocket 进行功能请求时，请求参数数据不是通过查询字符串发送给服务器的，而是通过 WebSocket
	 * 连接本身发送的。如果请求需要向服务器发送请求参数数据，那么在查询字符串中需要设置 "__data=1|true|yes"
	 * 进行指示，否则服务器无法触发目标功能的执行。如果请求通过 WebSocket 连接发送了请求数据，但是没有指定该参数，那么请求数据会被忽略。
	 */
	private static final String DATA_PARAM_NAME = "__data";

	/**
	 * 使用当前会话初始化 FunctionSocket 的新实例。
	 * 
	 * @param session
	 *            一个 Session 对象。
	 */
	public FunctionSocket(Session session) {
		super(session);
	}

	/**
	 * 检查请求查询字符串中是否包含了 __data=1|true|yes 参数。
	 * 
	 * @return 一个布尔值。
	 */
	private boolean hasSocketData() {
		return pittypat.primitives.Boolean.parse(this.getParameter(DATA_PARAM_NAME));
	}

	/**
	 * 执行目标 API 方法。
	 * 
	 * @param form
	 *            从 WebSocket 接收到的表单数据。
	 * 
	 * @return 关闭连接的代码。
	 */
	private int exec(MultiMap<String> form) {
		// 获取此前已经保存在请求中的 FunctionInfo
		FunctionInfo funcInfo = this.getRequestContext().getFuncInfo();

		try {
			// 在 .NET 中，通过异步处理可以有效的解决耗时问题，在 Java
			// 中，目前仅仅为了向客户端推送进度，如果处理比较耗时，将导致当前线程被阻塞。
			// 在以后可以将 Java 中改为异步操作。
			IJson result = funcInfo.invoke(null, form, new WebSocketAsyncProgress(this));
			// 发送结果到客户端
			this.send(result);
		} catch (Exception e) {
			// 记录错误信息
			LOGGER.error(e.getLocalizedMessage(), e);

			// 执行目标操作发生错误时向客户端发送异常信息
			try {
				this.sendError(e);
			} catch (Exception e1) {
				LOGGER.error(e1.getLocalizedMessage(), e1);
			}

			// 请求发生错误，关闭连接
			return CloseCodes.UNEXPECTED_CONDITION.getCode();
		}

		// 请求已经完成，正常关闭连接
		return CloseCodes.NORMAL_CLOSURE.getCode();
	}

	/**
	 * 当连接打开时要执行的方法。
	 * 
	 * @return 返回一个值，该值指示操作是否已经处理完毕。如果返回值不是 0，连接将被关闭。
	 */
	@Override
	protected int onOpen() {
		if (this.hasSocketData()) {
			// 需要从客户端接收表单数据，应该交给 onTextMessage 处理数据
			return 0;
		}

		// 没有需要从客户端接收的数据，直接执行目标功能方法
		return this.exec(null);
	}

	/**
	 * 由接收文本消息的继承类实现，用于处理接收到的文本消息。默认实现总是返回 true。
	 * 
	 * @param text
	 *            已经接收到的文本内容。
	 * @param last
	 *            指示当前文本是否是最后一部分。
	 * @return 返回一个值，该值指示操作是否已经处理完毕。如果返回值不是 0，连接将被关闭。
	 */
	@Override
	protected int onTextMessage(String text, boolean last) {
		// 通过 WebSocket 来处理功能请求时，请求参数不是在请求的查询字符串中，而是通过 WebSocket 将参数编码为
		// name=value&name=value... 的格式。
		MultiMap<String> form = null;

		if (text != null && text.length() > 0) {
			form = new MultiMap<String>();
			// 从接收到的字符串中解析得到的表单字段列表。
			decode(text, form);
		}

		// 使用解析到的表单字段列表执行目标功能方法
		return this.exec(form);
	}

	/**
	 * Utf8 Appendable abstract base class
	 *
	 * This abstract class wraps a standard {@link java.lang.Appendable} and
	 * provides methods to append UTF-8 encoded bytes, that are converted into
	 * characters.
	 *
	 * This class is stateful and up to 4 calls to {@link #append(byte)} may be
	 * needed before state a character is appended to the string buffer.
	 *
	 * The UTF-8 decoding is done by this class and no additional buffers or
	 * Readers are used. The UTF-8 code was inspired by
	 * http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
	 *
	 * License information for Bjoern Hoehrmann's code:
	 *
	 * Copyright (c) 2008-2009 Bjoern Hoehrmann &lt;bjoern@hoehrmann.de&gt;
	 * Permission is hereby granted, free of charge, to any person obtaining a
	 * copy of this software and associated documentation files (the
	 * "Software"), to deal in the Software without restriction, including
	 * without limitation the rights to use, copy, modify, merge, publish,
	 * distribute, sublicense, and/or sell copies of the Software, and to permit
	 * persons to whom the Software is furnished to do so, subject to the
	 * following conditions:
	 *
	 * The above copyright notice and this permission notice shall be included
	 * in all copies or substantial portions of the Software.
	 *
	 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
	 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
	 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
	 * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
	 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
	 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
	 * USE OR OTHER DEALINGS IN THE SOFTWARE.
	 **/
	abstract class Utf8Appendable {
		private static final char REPLACEMENT = '\ufffd';
		public final byte[] REPLACEMENT_UTF8 = new byte[] { (byte) 0xEF, (byte) 0xBF, (byte) 0xBD };
		private static final int UTF8_ACCEPT = 0;
		private static final int UTF8_REJECT = 12;

		protected final Appendable _appendable;
		protected int _state = UTF8_ACCEPT;

		private final byte[] BYTE_TABLE = {
				// The first part of the table maps bytes to character
				// classes that
				// to reduce the size of the transition table and create
				// bitmasks.
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
				1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
				7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
				2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 11, 6, 6, 6,
				5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 };

		private final byte[] TRANS_TABLE = {
				// The second part is a transition table that maps a
				// combination
				// of a state of the automaton and a character class to
				// a state.
				0, 12, 24, 36, 60, 96, 84, 12, 12, 12, 48, 72, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 0,
				12, 12, 12, 12, 12, 0, 12, 0, 12, 12, 12, 24, 12, 12, 12, 12, 12, 24, 12, 24, 12, 12, 12, 12, 12, 12,
				12, 12, 12, 24, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 12,
				12, 36, 12, 36, 12, 12, 12, 36, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, 12, 36, 12, 12, 12, 12, 12, 12,
				12, 12, 12, 12 };

		private final char[] hexArray = "0123456789ABCDEF".toCharArray();

		private int _codep;

		public Utf8Appendable(Appendable appendable) {
			_appendable = appendable;
		}

		public abstract int length();

		protected void reset() {
			_state = UTF8_ACCEPT;
		}

		private void checkCharAppend() throws IOException {
			if (_state != UTF8_ACCEPT) {
				_appendable.append(REPLACEMENT);
				int state = _state;
				_state = UTF8_ACCEPT;
				throw new NotUtf8Exception("char appended in state " + state);
			}
		}

		public void append(char c) {
			try {
				checkCharAppend();
				_appendable.append(c);
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}

		public void append(byte b) {
			try {
				appendByte(b);
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}

		public void append(byte[] b, int offset, int length) {
			try {
				int end = offset + length;
				for (int i = offset; i < end; i++)
					appendByte(b[i]);
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}

		private String toHexString(byte b) {
			char[] hexChars = new char[2];
			int v = b & 0xFF;
			hexChars[0] = hexArray[v >>> 4];
			hexChars[1] = hexArray[v & 0x0F];
			return new String(hexChars);
		}

		protected void appendByte(byte b) throws IOException {

			if (b > 0 && _state == UTF8_ACCEPT) {
				_appendable.append((char) (b & 0xFF));
			} else {
				int i = b & 0xFF;
				int type = BYTE_TABLE[i];
				_codep = _state == UTF8_ACCEPT ? (0xFF >> type) & i : (i & 0x3F) | (_codep << 6);
				int next = TRANS_TABLE[_state + type];

				switch (next) {
				case UTF8_ACCEPT:
					_state = next;
					if (_codep < Character.MIN_HIGH_SURROGATE) {
						_appendable.append((char) _codep);
					} else {
						for (char c : Character.toChars(_codep))
							_appendable.append(c);
					}
					break;

				case UTF8_REJECT:
					String reason = "byte " + toHexString(b) + " in state " + (_state / 12);
					_codep = 0;
					_state = UTF8_ACCEPT;
					_appendable.append(REPLACEMENT);
					throw new NotUtf8Exception(reason);

				default:
					_state = next;

				}
			}
		}

		public boolean isUtf8SequenceComplete() {
			return _state == UTF8_ACCEPT;
		}

		@SuppressWarnings("serial")
		final class NotUtf8Exception extends IllegalArgumentException {
			public NotUtf8Exception(String reason) {
				super("Not valid UTF8! " + reason);
			}
		}

		protected void checkState() {
			if (!isUtf8SequenceComplete()) {
				_codep = 0;
				_state = UTF8_ACCEPT;
				try {
					_appendable.append(REPLACEMENT);
				} catch (IOException e) {
					throw new RuntimeException(e);
				}
				throw new NotUtf8Exception("incomplete UTF8 sequence");
			}
		}

		public String toReplacedString() {
			if (!isUtf8SequenceComplete()) {
				_codep = 0;
				_state = UTF8_ACCEPT;
				try {
					_appendable.append(REPLACEMENT);
				} catch (IOException e) {
					throw new RuntimeException(e);
				}
			}
			return _appendable.toString();
		}
	}

	/**
	 * UTF-8 StringBuilder.
	 *
	 * This class wraps a standard {@link java.lang.StringBuilder} and provides
	 * methods to append UTF-8 encoded bytes, that are converted into
	 * characters.
	 *
	 * This class is stateful and up to 4 calls to {@link #append(byte)} may be
	 * needed before state a character is appended to the string buffer.
	 *
	 * The UTF-8 decoding is done by this class and no additional buffers or
	 * Readers are used. The UTF-8 code was inspired by
	 * http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
	 *
	 */
	final class Utf8StringBuilder extends Utf8Appendable {
		final StringBuilder _buffer;

		public Utf8StringBuilder() {
			super(new StringBuilder());
			_buffer = (StringBuilder) _appendable;
		}

		public Utf8StringBuilder(int capacity) {
			super(new StringBuilder(capacity));
			_buffer = (StringBuilder) _appendable;
		}

		@Override
		public int length() {
			return _buffer.length();
		}

		@Override
		public void reset() {
			super.reset();
			_buffer.setLength(0);
		}

		public StringBuilder getStringBuilder() {
			checkState();
			return _buffer;
		}

		@Override
		public String toString() {
			checkState();
			return _buffer.toString();
		}
	}

	/**
	 * @param c
	 *            An ASCII encoded character 0-9 a-f A-F
	 * @return The byte value of the character 0-16.
	 */
	private int convertHexDigit(char c) {
		int d = ((c & 0x1f) + ((c >> 6) * 0x19) - 0x10);
		if (d < 0 || d > 15)
			throw new NumberFormatException("!hex " + c);
		return d;
	}

	private void decode(String query, MultiMap<String> map) {
		Utf8StringBuilder buffer = new Utf8StringBuilder();
		synchronized (map) {
			int offset = 0;
			int length = query.length();
			String key = null;
			String value = null;

			int end = offset + length;
			for (int i = offset; i < end; i++) {
				char c = query.charAt(i);
				try {
					switch (c) {
					case '&':
						value = buffer.toReplacedString();
						buffer.reset();
						if (key != null) {
							map.add(key, value);
						} else if (value != null && value.length() > 0) {
							map.add(value, "");
						}
						key = null;
						value = null;
						break;

					case '=':
						if (key != null) {
							buffer.append(c);
							break;
						}
						key = buffer.toReplacedString();
						buffer.reset();
						break;

					case '+':
						buffer.append((byte) ' ');
						break;

					case '%':
						if (i + 2 < end) {
							if ('u' == query.charAt(i + 1)) {
								i++;
								if (i + 4 < end) {
									char top = query.charAt(++i);
									char hi = query.charAt(++i);
									char lo = query.charAt(++i);
									char bot = query.charAt(++i);
									buffer.getStringBuilder().append(
											Character.toChars((convertHexDigit(top) << 12) + (convertHexDigit(hi) << 8)
													+ (convertHexDigit(lo) << 4) + convertHexDigit(bot)));
								} else {
									buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT);
									i = end;
								}
							} else {
								char hi = query.charAt(++i);
								char lo = query.charAt(++i);
								buffer.append((byte) ((convertHexDigit(hi) << 4) + convertHexDigit(lo)));
							}
						} else {
							buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT);
							i = end;
						}
						break;

					default:
						buffer.append(c);
						break;
					}
				} catch (NumberFormatException e) {
					buffer.append(buffer.REPLACEMENT_UTF8, 0, 3);
				}
			}

			if (key != null) {
				value = buffer.toReplacedString();
				buffer.reset();
				map.add(key, value);
			} else if (buffer.length() > 0) {
				map.add(buffer.toReplacedString(), "");
			}
		}
	}
}
